Implementing the Runnable Interface in Java
Course: Core Java Training
Topic: Implementing the Runnable Interface
Course Link: Core Java Training Course
Demo Link: Register for a Course Demo
JustAcademy Website: Visit JustAcademy
1. Introduction to the Runnable Interface
Java supports multithreading, which allows multiple threads to make progress concurrently within a program. One common way to define a task for a thread is to implement the Runnable interface.
The Runnable interface belongs to the java.lang package and represents a task that can be executed by a thread. It defines a single abstract method named run(). A class that implements Runnable provides the code that should execute when the task runs.
Implementing Runnable separates the task's logic from the thread that executes it. Instead of extending the Thread class, a class can implement Runnable and pass its object to a Thread instance. This approach is especially useful when a class already extends another class or when the same task needs to be submitted to different execution mechanisms.
2. Learning Objectives
- Understand the purpose of the
Runnable interface.
- Learn how to implement and override the
run() method.
- Create and start a thread using a
Runnable object.
- Understand the difference between
start() and run().
- Compare implementing
Runnable with extending Thread.
- Use anonymous classes, lambda expressions, and method references with
Runnable.
- Understand thread synchronization, interruption, and common implementation mistakes.
- Apply the
Runnable interface in practical Java programs.
3. What Is the Runnable Interface?
Runnable is a functional interface used to represent an operation that does not return a result. Its main method is:
public interface Runnable {
void run();
}
The interface is annotated with @FunctionalInterface in the Java API, which means it has one abstract method and can be implemented using a lambda expression or method reference.
When a Runnable object is supplied to a Thread and that thread is started, the task's run() method is invoked in the new thread of execution.
Important Characteristics
- Package:
java.lang
- Main method:
run()
- Return type:
void
- Parameters: None
- Purpose: Defines a task to execute.
- Thread creation: A
Runnable object alone does not start a new thread.
- Implementation: Can be implemented by a class, anonymous class, or lambda expression.
4. Why Use the Runnable Interface?
Java allows a class to extend only one class. If a class extends Thread, it cannot extend another class at the same time. Implementing Runnable avoids this limitation because a class can implement interfaces while extending a class.
Another important benefit is separation of concerns. The task contains the business logic, while the Thread object manages the execution thread. The task can also be reused with executor services and thread pools.
Advantages of Runnable
- Separates task logic from thread management.
- Allows the class to extend another class.
- Makes task objects easier to reuse.
- Supports lambda expressions and method references.
- Works with thread pools and executor services.
- Encourages a flexible design for concurrent applications.
5. Steps to Implement the Runnable Interface
Follow these steps to create and execute a thread using Runnable:
- Create a class that implements
Runnable.
- Override the
run() method with the task's logic.
- Create an object of the class.
- Pass the object to the
Thread constructor.
- Call
start() on the thread object.
- Allow the JVM scheduler to execute the task.
6. Basic Example of Implementing Runnable
The following program demonstrates a simple task that prints a message from a separate thread.
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Task is running in a separate thread");
}
}
public class Main {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task);
thread.start();
}
}
Output:
Task is running in a separate thread
Explanation
MyTask implements the Runnable interface.
- The
run() method contains the task to execute.
new MyTask() creates the task object.
new Thread(task) creates a thread configured to execute the task.
thread.start() starts the new thread, which then invokes the task's run() method.
7. Example: Runnable with a Loop
A Runnable task can contain loops, calculations, method calls, or other application logic just like an ordinary Java method.
class NumberPrinter implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Number: " + i);
}
}
}
public class Main {
public static void main(String[] args) {
NumberPrinter task = new NumberPrinter();
Thread thread = new Thread(task);
thread.start();
}
}
Output:
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5
This output is typical for this single task, but when several threads print simultaneously, their output can interleave in different orders depending on scheduling.
8. Running Multiple Threads with Runnable
The same Runnable implementation can be used to create multiple thread objects. Each thread independently executes the task, although shared objects or variables used by those tasks may require synchronization.
class Worker implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(
Thread.currentThread().getName() + " - " + i
);
}
}
}
public class Main {
public static void main(String[] args) {
Worker task = new Worker();
Thread first = new Thread(task, "Worker-A");
Thread second = new Thread(task, "Worker-B");
first.start();
second.start();
}
}
Possible output:
Worker-A - 1
Worker-B - 1
Worker-A - 2
Worker-B - 2
Worker-B - 3
Worker-A - 3
The actual order may differ. Both threads execute the same task object. This is safe in this example because the task does not maintain mutable instance state; if a shared task modifies common data, appropriate thread-safety measures may be necessary.
9. Difference Between start() and run()
Understanding the difference between start() and run() is essential when working with the Runnable interface.
| Feature |
start() |
run() |
| Purpose |
Starts a new thread of execution. |
Contains or invokes the task's execution logic. |
| Thread behavior |
Schedules execution on a separate thread. |
A direct method call executes on the calling thread. |
| JVM behavior |
The new thread invokes its target's run() method. |
Does not itself create a new thread. |
| Typical use |
Call it on a newly created Thread. |
Override it in a task or call it indirectly through a thread. |
Example
class MyTask implements Runnable {
@Override
public void run() {
System.out.println(
"Executing on: " + Thread.currentThread().getName()
);
}
}
public class Main {
public static void main(String[] args) {
MyTask task = new MyTask();
task.run();
Thread thread = new Thread(task, "Background-Worker");
thread.start();
}
}
Possible output:
Executing on: main
Executing on: Background-Worker
The direct call to task.run() runs on the main thread. The call to thread.start() runs the task on a separate thread.
10. Implementing Runnable with a Lambda Expression
Because Runnable is a functional interface, Java allows a lambda expression to provide the implementation of its single abstract method. This approach is concise and common in modern Java applications.
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Runnable using a lambda expression");
};
Thread thread = new Thread(task);
thread.start();
}
}
Output:
Runnable using a lambda expression
The lambda expression acts as the implementation of run(). It does not create or start a thread by itself; the Thread object and its start() method provide the separate execution.
11. Implementing Runnable with an Anonymous Class
An anonymous class can implement Runnable without declaring a separate named class. This style is useful when the task is small and only needed in one location.
public class Main {
public static void main(String[] args) {
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Task from anonymous class");
}
};
new Thread(task).start();
}
}
Output:
Task from anonymous class
For short tasks, a lambda expression is usually more concise. Anonymous classes remain useful when the implementation requires additional methods or more complex class behavior.
12. Implementing Runnable with a Method Reference
A method reference can be used when an existing method matches the signature required by Runnable. For example, a method that takes no arguments and returns void can be referenced as a task.
public class Main {
static void printMessage() {
System.out.println("Executing an existing method");
}
public static void main(String[] args) {
Runnable task = Main::printMessage;
Thread thread = new Thread(task);
thread.start();
}
}
Output:
Executing an existing method
The method reference Main::printMessage supplies the implementation for run(). The method executes on the thread that runs the task.
13. Runnable Versus Extending the Thread Class
Java offers two common approaches for defining thread tasks: implementing Runnable and extending Thread. They are related, but they represent different design choices.
| Feature |
Implementing Runnable |
Extending Thread |
| Relationship |
Defines a task that can be executed by a thread. |
Defines a specialized thread class. |
| Inheritance |
The class can extend another class. |
The class already extends Thread. |
| Task reuse |
The same task can be passed to multiple thread objects or execution frameworks. |
The task is coupled to the thread subclass. |
| Thread management |
Separate from the task implementation. |
Combined with the subclass. |
| Lambda support |
Yes, because Runnable is a functional interface. |
A subclass of Thread is not itself a functional interface. |
| Recommended use |
Usually preferred when defining task logic. |
Useful when genuinely customizing thread behavior. |
14. Using Runnable with Thread.sleep()
The sleep() method pauses the currently executing thread for a specified time. It is often used in demonstrations, timed tasks, and simple simulations. It does not release monitor locks that the current thread holds.
class TimerTaskExample implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("Count: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Task interrupted");
return;
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new TimerTaskExample());
thread.start();
}
}
Output:
Count: 1
Count: 2
Count: 3
The values are printed approximately one second apart. The sleep duration is not a guarantee of exact scheduling time. The example also restores the interrupt status and exits the task if an interruption occurs.
15. Exception Handling in Runnable
The run() method does not declare checked exceptions in its method signature. Therefore, checked exceptions raised inside the task must be caught and handled within the implementation or converted into an appropriate unchecked exception.
class FileProcessingTask implements Runnable {
@Override
public void run() {
try {
processFile();
} catch (java.io.IOException e) {
System.err.println("File processing failed: " + e.getMessage());
}
}
private void processFile() throws java.io.IOException {
System.out.println("Processing file...");
// File-processing logic would be placed here.
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new FileProcessingTask());
thread.start();
}
}
Output:
Processing file...
In this example, processFile() declares that it may throw IOException, so the task handles that checked exception inside run(). In production applications, exception handling should also record useful diagnostic information and communicate failures to the appropriate application layer.
16. Thread Safety When Using Runnable
Implementing Runnable does not automatically make a task thread-safe. If multiple threads access and modify the same mutable object, race conditions may occur unless access is coordinated correctly.
Example: Synchronized Counter
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
class CounterTask implements Runnable {
private final Counter counter;
CounterTask(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread first = new Thread(new CounterTask(counter));
Thread second = new Thread(new CounterTask(counter));
first.start();
second.start();
first.join();
second.join();
System.out.println(counter.getCount());
}
}
Output:
2000
The two tasks share the same Counter object. Its synchronized methods protect the shared counter from lost updates, and the calls to join() ensure both threads complete before the final value is read.
17. Using Runnable with ExecutorService
In real applications, creating a new thread manually for every task may be inefficient. Java's executor framework allows tasks to be submitted to a thread pool, which manages thread creation and reuse.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
Runnable task = () -> {
System.out.println(
"Task executed by " + Thread.currentThread().getName()
);
};
executor.submit(task);
executor.submit(task);
executor.shutdown();
}
}
Possible output:
Task executed by pool-1-thread-1
Task executed by pool-1-thread-2
The actual thread names and execution order may vary. The executor schedules submitted tasks using its available worker threads. Calling shutdown() stops accepting new tasks while allowing previously submitted tasks to complete.
Note: If an application needs to wait until all submitted tasks have completed, it can use awaitTermination(), futures, or another coordination mechanism.
18. Runnable and Callable: Key Differences
Runnable is designed for a task that does not return a result. If a task needs to return a value or declare a checked exception through its method signature, Java provides the Callable<V> interface.
| Feature |
Runnable |
Callable |
| Method |
run() |
call() |
| Return value |
No return value; return type is void. |
Returns a value of type V. |
| Checked exceptions |
Cannot declare checked exceptions in run(). |
call() can declare checked exceptions. |
| Typical use |
Tasks such as logging, sending messages, or updating data. |
Tasks that calculate and return results. |
| Executor usage |
Can be submitted to an executor. |
Can be submitted to an executor to obtain a Future result. |
19. Common Mistakes When Implementing Runnable
| Mistake |
Why It Is a Problem |
Correct Approach |
Calling run() directly |
The task executes on the current thread. |
Pass the task to a Thread and call start(). |
| Assuming Runnable starts a thread automatically |
The interface only defines task behavior. |
Use a Thread or an executor service to run it. |
| Ignoring interruption |
Tasks may fail to stop when cancellation is requested. |
Handle InterruptedException and restore interrupt status when appropriate. |
| Sharing mutable data without protection |
Concurrent modifications can produce inconsistent results. |
Use synchronization, locks, atomic variables, or concurrent collections as appropriate. |
| Assuming a fixed output order |
Thread scheduling is nondeterministic. |
Use explicit coordination when ordering matters. |
| Creating too many threads |
Excessive threads consume resources and increase scheduling overhead. |
Consider a suitable executor or thread pool. |
| Swallowing exceptions silently |
Failures become difficult to detect and diagnose. |
Log, report, or otherwise handle errors meaningfully. |
20. Best Practices for Runnable
- Prefer implementing
Runnable when defining a task rather than extending Thread without a specific reason.
- Keep the
run() method focused on the task's execution logic.
- Use meaningful thread names when manually creating threads.
- Use executor services for managing collections of tasks in many production applications.
- Protect shared mutable state using suitable concurrency mechanisms.
- Do not rely on thread scheduling to establish a particular execution order.
- Handle interruption as a cooperative cancellation request.
- Use
Callable and Future when results or exception propagation are required.
- Avoid deprecated thread-control methods such as
stop(), suspend(), and resume().
- Test concurrent code carefully because race conditions may not appear consistently.
21. Practice Exercises
- Create a class named
GreetingTask that implements Runnable and prints a greeting message.
- Implement a
Runnable class that prints numbers from 1 to 10.
- Create two threads using the same task object and observe how their output interleaves.
- Write a lambda expression that implements
Runnable and prints the current thread name.
- Create a task that pauses using
Thread.sleep() and handles interruption correctly.
- Build a shared counter example and protect its updates using synchronization.
- Submit several
Runnable tasks to an ExecutorService and shut the executor down properly.
- Explain the difference between implementing
Runnable and extending Thread.
22. Interview Questions and Answers
Q1. What is the Runnable interface in Java?
Answer: Runnable is a functional interface in java.lang that defines the run() method. It represents a task that can be executed by a thread and does not return a result.
Q2. Why is Runnable preferred over extending Thread in many cases?
Answer: It separates task logic from thread management and allows the implementing class to extend another class. It also supports task reuse with thread pools and executor services.
Q3. Does implementing Runnable create a new thread?
Answer: No. Implementing Runnable defines the task. A new thread can be created by passing the task to a Thread object and calling start(), or the task can be submitted to an executor.
Q4. What is the purpose of the run() method?
Answer: The run() method contains the code that should execute when the task is run. When a thread with a Runnable target is started, that thread invokes the target's run() method.
Q5. What happens if run() is called directly?
Answer: It behaves like an ordinary method call and executes on the calling thread. It does not start a new thread.
Q6. Can Runnable be implemented using a lambda expression?
Answer: Yes. Runnable is a functional interface with one abstract method, so a lambda expression can provide its implementation.
Q7. Can multiple threads use the same Runnable object?
Answer: Yes. Multiple thread objects can use the same Runnable instance. If the task shares or modifies mutable state, that state must be managed safely.
Q8. Can the run() method return a value?
Answer: No. Its return type is void. Use Callable<V> when a task needs to return a value.
Q9. How does Runnable work with ExecutorService?
Answer: An executor accepts Runnable tasks and schedules them for execution, often using a pool of reusable worker threads. This separates task submission from thread management.
Q10. Is Runnable automatically thread-safe?
Answer: No. Thread safety depends on the task implementation and the data it accesses. Shared mutable state may require synchronization or other concurrency controls.
23. Quick Revision Checklist
Runnable belongs to the java.lang package.
- Its single abstract method is
void run().
- A Runnable object defines a task but does not start a thread by itself.
- Pass the task to a
Thread constructor and call start() to execute it on a new thread.
- Calling
run() directly does not create a new thread.
- Runnable can be implemented using a normal class, anonymous class, lambda expression, or method reference.
- Multiple threads can execute the same task object.
- Shared mutable data must be protected when concurrent access can cause race conditions.
- Use
Callable when a task needs to return a result.
- Consider
ExecutorService for managing multiple tasks.
24. Conclusion
The Runnable interface is a fundamental part of Java multithreading. It provides a simple way to define a task independently from the thread that executes it. By implementing run() and passing the task to a Thread or an executor service, developers can build reusable and maintainable concurrent applications.
Remember that implementing Runnable does not automatically create a new thread or guarantee thread safety. Correct use of start(), appropriate coordination, interruption handling, and protection of shared data are essential for reliable multithreaded programming.
Continue learning with the Core Java Training Course, register for a course demo, or visit the JustAcademy website.